package com.bullsora.kindlerss.fulfillment;


import com.sun.syndication.feed.synd.SyndContent
import com.sun.syndication.feed.synd.SyndEntry
import com.sun.syndication.feed.synd.SyndFeed
import com.sun.syndication.io.SyndFeedInput
import com.sun.syndication.io.XmlReader
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.concurrent.ScheduledFuture
import org.apache.log4j.Logger
import org.springframework.scheduling.TaskScheduler
import org.springframework.scheduling.support.CronTrigger
import com.bullsora.kindlerss.model.*

/**
 * @author Sorin Buliarca
 */
public class FulfillmentService {

  public static final String FILE_SUFFIX_PATTERN = "EE_HH_mm_dd_MMM";

  private static def LOG = Logger.getLogger(this)

  TaskScheduler taskScheduler

  private Map<Long, ScheduledFuture> tasksMap = new HashMap<Long, ScheduledFuture>();

  StructuredHtmlService structuredHtmlService

  def feedUpdateThreshold
  
  public void init() {
    LOG.info("Initializing subscription tasks");
    Subscription.findAll().each { subscription ->
      scheduleFulfillmentTask(subscription);
    }
  }

  public void destroy() {
    LOG.info("Destroying subscription tasks");
    tasksMap.values().each { task ->
      task.cancel(false);
    }
  }

  def scheduleFulfillmentTask(Subscription subscription) {
    LOG.info("Scheduling fulfillment task for subscription ${subscription.id}");
    ScheduledFuture existingScheduledFuture = tasksMap.get(subscription.id);
    if (existingScheduledFuture != null) {
      existingScheduledFuture.cancel(false);
    }

    def timeZone = subscription?.appUser?.timeZone ? TimeZone.getTimeZone(subscription.appUser.timeZone) : TimeZone.getDefault()
    ScheduledFuture newScheduledFuture = taskScheduler.schedule(
        new FulfillSubscriptionTask(subscriptionId: subscription.id, fulfillmentService: this),
        new CronTrigger(subscription.sendCronExpression, timeZone)
    );

    tasksMap.put(subscription.id, newScheduledFuture);
  }

  def cancelFulfillmentTask(Subscription subscription) {
    LOG.info("Canceling fulfillment task for subscription ${subscription.id}")
    tasksMap[subscription.id]?.cancel(false)
    tasksMap.remove(subscription.id)
  }

  private def updateFeed(FeedUrl feedUrl) {
    if (!shouldUpdate(feedUrl)) {
      LOG.info("The feed url ${feedUrl.url} was updated at ${feedUrl.lastUpdated}. Skipping update as threshold is ${feedUpdateThreshold}");
      return;
    }

    LOG.info("Checking new entries for feed ${feedUrl.url}");
    try {
      SyndFeed syndFeed = new SyndFeedInput().build(new XmlReader(new URL(feedUrl.url)));
      syndFeed.entries.each { SyndEntry receivedEntry ->
        if (!FeedEntry.findByFeedUrlAndLink(feedUrl, receivedEntry.link)) {
          addFeedEntry(feedUrl, receivedEntry);
        }
      }

      feedUrl.lastUpdated = new Date();
      feedUrl.save();
    } catch (e) {
      LOG.error("Error while reading feed ${feedUrl}", e);
    }
  }

  private boolean shouldUpdate(FeedUrl feedUrl) {
    if (feedUrl.getLastUpdated() == null || !feedUpdateThreshold) {
      return true;
    }
    return System.currentTimeMillis() - feedUrl.lastUpdated.time > feedUpdateThreshold * 1000;
  }

  private void addFeedEntry(FeedUrl feedUrl, SyndEntry receivedEntry) {
    FeedEntry newFeedEntry = new FeedEntry(
        feedUrl: feedUrl,
        link: receivedEntry.link,
        title: receivedEntry.title,
        author: receivedEntry.author,
        publishedDate: receivedEntry.publishedDate,
        receiveDate: new Date()
    );

    if (receivedEntry.description) {
      newFeedEntry.description = receivedEntry.description.value
    }
    else if (receivedEntry.contents) {
      newFeedEntry.description = ((SyndContent) receivedEntry.contents[0]).value
    }

    LOG.info("Adding new entry ${newFeedEntry.properties}");
    newFeedEntry.save();
  }

  def sendNewEntries(Long subscriptionId) {
    Subscription.withNewSession {
      Subscription subscription = Subscription.get(subscriptionId);
      if (!subscription) {
        LOG.warn("Could not find subscription with id " + subscriptionId);
        return;
      }

      sendNewEntriesForSubscription(subscription);
      subscription.save(flush: true);
    }
  }

  private def sendNewEntriesForSubscription(Subscription subscription) {
    LOG.info("Sending new entries for subscription ${subscription.prefix}")

    subscription.feeds.each { Feed feed ->
      updateFeed(feed.url);
    }
    
    computeNewEntries(subscription);
    if (subscription.nonEmptyFeeds) {
      String html = structuredHtmlService.generateHtmlForSubscription(subscription);
      AppUser user = subscription.getAppUser();

      DateFormat format = new SimpleDateFormat(FILE_SUFFIX_PATTERN);
      format.setTimeZone(TimeZone.getTimeZone(user.timeZone));
      def fileName = "${subscription.prefix}_${format.format(new Date())}.html"

      LOG.debug("""------ Sending generated attachment to ${user.kindleAccount}@free.kindle.com: \n
                   ---------------------------------------- \n
                   ${html}
                   \n ------------------------------------------ """)
      
      sendMail {
        multipart true
        to "${user.kindleAccount}@free.kindle.com"
        subject "feed"
        body "feed"
        attachBytes fileName, "text/xml", html.getBytes("UTF-8")
      }

    }
    else {
      LOG.info("No new entries to send found for subscription ${subscription.prefix}");
    }

    subscription.lastChecked = new Date()
  }

  private void computeNewEntries(Subscription subscription) {
    List<Feed> nonEmptyFeeds = new ArrayList<Feed>()

    for (Feed feed: subscription.feeds) {
      List<FeedEntry> feedNewEntries = FeedEntry.findAllByFeedUrlAndReceiveDateGreaterThan(feed.url, subscription.lastChecked)
      if (feedNewEntries) {
        feed.newEntries = new HashSet<FeedEntry>(feedNewEntries)
        nonEmptyFeeds.add(feed)
      }
    }

    subscription.nonEmptyFeeds = nonEmptyFeeds
  }

  private class FulfillSubscriptionTask implements Runnable {

    Long subscriptionId

    FulfillmentService fulfillmentService

    @Override
    public void run() {
      try {
        fulfillmentService.sendNewEntries(subscriptionId);
      }
      catch (e) {
        LOG.error("Error while sending new entries for subscription ${subscriptionId}", e)
      }
    }
  }

}
